import { Injectable } from '@angular/core';
import { AbstractControl, NgModel, ValidatorFn, ValidationErrors, FormControl } from '@angular/forms';

@Injectable()
export class ValidatorFactoryService {
    constructor() { }

    /**
     * Gets the ForbiddenValueValidator to check whether the input contains the restrict value.
     * @param nameRe The Regular Expression string to identify the restrict value.
     */
    getForbiddenValueValidator(nameRe: RegExp): ValidatorFn {
        return (control: AbstractControl): { [key: string]: any } => {
            const forbidden = nameRe.test(control.value);
            return forbidden ? { 'forbiddenValue': { forbiddenValue: nameRe.source, value: control.value } } : null;
        }
    }

    /**
     * Checks if the input number | date meets start >= end in the array. 
     * If it does meet then the error returned.
     */
    notInRange(control: AbstractControl): ValidationErrors | null {
        let result;

        if (control.value instanceof Array) {
            const range: (number | Date)[] = control.value;

            if (range.length == 2 && range[0] > range[1]) {
                result = { 'notInRange': { notInRange: true, value: control.value } };
            }
        }

        return result;
    }

    /**
     * Checks all form group controller values are identical by control names.
     * 
     * @param formControlNames All the names of form control in the view.
     * @param errorKey The error key which can be used to determine if this validation fails. The default value is valueNotMatch.
     */
    getValueNotMatchValidator(formControlNames: string[], errorKey: string = 'valueNotMatch'): ValidatorFn {
        return (control: AbstractControl): { [key: string]: any } => {
            // Gets all form controls via name.
            let formControls = formControlNames.map((name: string) => control.get(name));

            // Checks if all the values are identical.
            if (formControls.every((formControl: FormControl, index: number, array: FormControl[]) => formControl.value === array[0].value)) {
                // Clears the last form control error if all values are identical.
                //
                // Please be aware:
                // Angular does not provide the methods like patchError/removeError whhich can work on one single error and leave the others untouched.
                // setErrors method clears all the error with one shot.
                // That's why we also do this on last form control.
                //
                // We olny need to check if all the values are identical.
                // We can apply any validation to the other form controls, like required, minlength and so on.
                // If the last form control value matches them then it will also satisfy all the validations.
                // That's why we only use setErrors method on the last one.
                formControls[formControls.length - 1].setErrors(null);

                return null;
            } else {
                // Prepares the return error.
                let error = { [errorKey]: true, value: control.value };

                // Sets the last form control error. This is for UI validation usage.
                // This would make the last form control show error response.
                formControls[formControls.length - 1].setErrors(error);

                // Returns the error. This error only applies to the from group not any form control.
                return error;
            }
        }
    }

    /**
     * Gets the ValueNotMatchValidator to check whether two input values are identical.
     * @param referencedFieldModel The reference field model whose value will be compared to fieldModel value.
     * @param fieldModel The current field model.
     */
    getValueNotMatchValidatorDeprecated(referencedFieldModel: NgModel | AbstractControl, fieldModel: NgModel | AbstractControl): ValidatorFn {
        return (control: AbstractControl): { [key: string]: any } => {
            const notMatched = referencedFieldModel.value !== fieldModel.value;

            // For Reactive Form Validation Example:
            // this.formGroup.controls.confirmPassword.setValidators([
            //     Validators.required,
            //     this.validatorFactory.getValueNotMatchValidator(this.formGroup.controls.password, this.formGroup.controls.confirmPassword)
            // ]);

            // BY NOW, we need another line of the code.
            // this.formGroup.controls.password.valueChanges.subscribe(() =>
            // this.validatorFactory.setValueNotMatchFieldModelError(this.formGroup.controls.password, this.formGroup.controls.confirmPassword));

            // BY NOW, I don't think each fieldModel value change applies one time subscribtion is a good idea. Before I can avoid to subscribe 
            // multiple times I leave it here. Maybe I can figure it out later.
            // Subscribes the valueChanges of the reference field model. If the reference field value changes also triggers current field's validity change.
            // referencedFieldModel.valueChanges.subscribe(() => this.setValueNotMatchFieldModelError(referencedFieldModel, fieldModel));

            return notMatched ? this.getValueNotMatchDefaultError(fieldModel.value) : null;
        };
    }

    /**
     * Sets the field model error for the ValueNotMatchValidator.
     * @param fieldModel The input field. The NgModel field for template-driven form or the AbstractControl for the reactive form.
     * @param fieldModelErrorToChange Another input field which value will be compared to the fieldModel value. The model error will be set based on the result.
     */
    setValueNotMatchFieldModelError(fieldModel: NgModel | AbstractControl, fieldModelErrorToChange: NgModel | AbstractControl): void {
        let notMatched = fieldModel.value !== fieldModelErrorToChange.value;

        if (notMatched) {
            // If two values are NOT identical.
            // Sets the valueNotMatch error on referencedFieldModel field if needed.
            if (!fieldModelErrorToChange.errors || (fieldModelErrorToChange.errors && !fieldModelErrorToChange.errors.valueNotMatch)) {
                if (fieldModelErrorToChange instanceof NgModel) {
                    fieldModelErrorToChange.control.setErrors(this.getValueNotMatchDefaultError(fieldModelErrorToChange.value));
                } else if (fieldModelErrorToChange instanceof AbstractControl) {
                    fieldModelErrorToChange.setErrors(this.getValueNotMatchDefaultError(fieldModelErrorToChange.value));
                }
            }
        } else {
            // If two values are identical, removes the valueNotMatch error from referencedFieldModel field.
            // Otherwise sets the valueNotMatch error on referencedFieldModel field.
            if (fieldModelErrorToChange.errors && fieldModelErrorToChange.errors.valueNotMatch) {
                // Removes the valueNotMatch error from referencedFieldModel field.
                delete fieldModelErrorToChange.errors.valueNotMatch;

                // If there's no more any other errors.
                if (fieldModelErrorToChange.errors !== {}) {
                    // Changes the referencedFieldModel field validity.
                    if (fieldModelErrorToChange instanceof NgModel) {
                        fieldModelErrorToChange.control.updateValueAndValidity();
                    } else if (fieldModelErrorToChange instanceof AbstractControl) {
                        fieldModelErrorToChange.updateValueAndValidity();
                    }
                }
            }
        }
    }

    /**
     * Gets the defaut error.
     * @param inputValue The form field input value.
     * @return An object contains property can be accessed via [fieldName].errors.[propertyName].
     */
    getValueNotMatchDefaultError(inputValue: any): any {
        return { 'valueNotMatch': { value: inputValue } };
    }
}